﻿#region License

// This file is part of Grawlix.
// 
// Grawlix is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// Grawlix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public
// License along with Grawlix.  If not, see
// <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Grawlix.Application.Gui.Messages;
using Grawlix.Geometry;
using Grawlix.Services.Rendering;

namespace Grawlix.Application.Gui
{
	/// <summary>
	/// A class that keeps track of GUI controls.
	/// </summary>
	public class GuiManager : IDisposable
	{
		/// <summary>
		/// Whether the object has been disposed.
		/// </summary>
		private bool _disposed;

		/// <summary>
		/// The control that has keyboard focus.
		/// </summary>
		private Control _keyboardFocus;

		/// <summary>
		/// The last control that had mouse focus.
		/// </summary>
		private TestMouseResult _lastMouseResult;

		/// <summary>
		/// The top level visible control in the hierarchy.
		/// </summary>
		private Control _topLevel;

		/// <summary>
		/// The last area that was rendered to.
		/// </summary>
		private Rect _viewport = new Rect((-1), (-1), 1, 1);

		/// <summary>
		/// Gets or sets the control that has keyboard focus.
		/// </summary>
		/// <value>The control that has keyboard focus.</value>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed.</exception>
		public Control KeyboardFocus
		{
			get
			{
				if (_disposed)
					throw new ObjectDisposedException("GuiManager");
				return _keyboardFocus;
			}
			set
			{
				if (_disposed)
					throw new ObjectDisposedException("GuiManager");
				_keyboardFocus = value;
			}
		}

		/// <summary>
		/// Gets or sets the top level control in the hierarchy.
		/// </summary>
		/// <value>The top level control in the hierarchy of visible
		/// controls.</value>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed.</exception>
		public Control TopLevel
		{
			get
			{
				if (_disposed)
					throw new ObjectDisposedException("GuiManager");
				return _topLevel;
			}
			set
			{
				if (_disposed)
					throw new ObjectDisposedException("GuiManager");
				_topLevel = value;
			}
		}

		/// <summary>
		/// Gets the last area rendered to.
		/// </summary>
		/// <value>The last area rendered to.</value>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed.</exception>
		public Rect Viewport
		{
			get
			{
				if (_disposed)
					throw new ObjectDisposedException("GuiManager");
				return _viewport;
			}
		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Releases unmanaged and - optionally - managed resources
		/// </summary>
		/// <param name="disposing"><c>true</c> to release both managed
		/// and unmanaged resources; <c>false</c> to release only
		/// unmanaged resources.</param>
		protected virtual void Dispose(bool disposing)
		{
			if (!_disposed)
			{
				if (disposing)
				{
					IDisposable temp;
					if (_topLevel != null)
					{
						temp = (IDisposable)_topLevel;
						_topLevel = null;
						temp.Dispose();
					}
					if (_keyboardFocus != null)
					{
						temp = (IDisposable)_keyboardFocus;
						_keyboardFocus = null;
						temp.Dispose();
					}
					if (_lastMouseResult.Focus != null)
					{
						temp = (IDisposable)_lastMouseResult.Focus;
						_lastMouseResult = default(TestMouseResult);
						temp.Dispose();
					}
				}
				_disposed = true;
			}
		}

		/// <summary>
		/// Finds the control that has mouse focus.
		/// </summary>
		/// <param name="position">The position of the cursor.</param>
		/// <param name="bounds">The screen area.</param>
		/// <returns></returns>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed.</exception>
		private TestMouseResult FindMouseFocus(Vector2 position,
		                                       Rect bounds)
		{
			if (_disposed)
				throw new ObjectDisposedException("GuiManager");
			if (!bounds.Contains(position))
				return default(TestMouseResult);
			TestMouseResult result;
			if (TopLevel != null)
			{
				result = TopLevel.TestMousePosition(position, bounds);
				Debug.Assert(result.Focus != null);
				Debug.Assert(result.Bounds.Contains(result.Position));
			}
			else
			{
				result = new TestMouseResult
					{Position = position, Bounds = bounds};
			}
			if (_lastMouseResult.Focus != result.Focus)
			{
				if (_lastMouseResult.Focus != null)
				{
					_lastMouseResult.Focus.OnMouseLeave(
						_lastMouseResult.Position, _lastMouseResult.Bounds);
				}
				if (result.Focus != null)
					result.Focus.OnMouseEnter(position, bounds);
			}
			_lastMouseResult = result;
			return _lastMouseResult;
		}

		/// <summary>
		/// Runs a character input message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		/// <exception cref="InvalidOperationException">
		/// An unexpected input message was received.</exception>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		[SuppressMessage("Microsoft.Performance", "CA1822",
			Justification = "Called as a multimethod.")]
		public void HandleInputMessage(InputMessage message)
		{
			throw new InvalidOperationException(
				"Unexpected input message type " + message + ".");
		}

		/// <summary>
		/// Runs a character input message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		public void HandleInputMessage(
			CharacterInputMessage message)
		{
			Control control = _keyboardFocus;
			if (control != null)
				control.OnCharacter(message.Character);
		}

		/// <summary>
		/// Runs a key down message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		public void HandleInputMessage(KeyDownInputMessage message)
		{
			Control control = _keyboardFocus;
			if (control != null)
				control.OnKeyDown(message.Code);
		}

		/// <summary>
		/// Runs a key up message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		public void HandleInputMessage(KeyUpInputMessage message)
		{
			Control control = _keyboardFocus;
			if (control != null)
				control.OnKeyUp(message.Code);
		}

		/// <summary>
		/// Runs a mouse down message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		public void HandleInputMessage(
			MouseDownInputMessage message)
		{
			TestMouseResult result = FindMouseFocus(
				message.Position, _viewport);
			if (result.Focus != null)
			{
				result.Focus.OnMouseDown(result.Position, message.Code,
				                         result.Bounds);
			}
		}

		/// <summary>
		/// Runs a mouse move message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		public void HandleInputMessage(
			MouseMoveInputMessage message)
		{
			TestMouseResult result = FindMouseFocus(
				message.Position, _viewport);
			if (result.Focus != null)
			{
				Vector2 delta =
					_viewport.ProjectDisplacement(message.Delta,
					                              result.Bounds);
				result.Focus.OnMouseMove(result.Position, delta,
				                         result.Bounds);
			}
		}

		/// <summary>
		/// Runs a mouse up message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		public void HandleInputMessage(MouseUpInputMessage message)
		{
			TestMouseResult result = FindMouseFocus(
				message.Position, _viewport);
			if (result.Focus != null)
			{
				result.Focus.OnMouseUp(result.Position, message.Code,
				                       result.Bounds);
			}
		}

		/// <summary>
		/// Runs a mouse wheel message through the GUI.
		/// </summary>
		/// <param name="message">The message to run.</param>
		[SuppressMessage("Microsoft.Design", "CA1011",
			Justification = "Called as multimethod.")]
		public void HandleInputMessage(
			MouseWheelInputMessage message)
		{
			TestMouseResult result = FindMouseFocus(
				message.Position, _viewport);
			if (result.Focus != null)
			{
				result.Focus.OnMouseWheel(result.Position, message.Ticks,
				                          result.Bounds);
			}
		}

		/// <summary>
		/// Renders to the specified device.
		/// </summary>
		/// <param name="device">The device to render to.</param>
		/// <param name="area">The area to render.</param>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed.</exception>
		/// <exception cref="ArgumentNullException"><c>device</c> is
		/// null.</exception>
		public void Render(IRenderDevice device, Rect area)
		{
			if (_disposed)
				throw new ObjectDisposedException("GuiManager");
			if (device == null)
				throw new ArgumentNullException("device");
			_viewport = area;
			Control top = TopLevel;
			if (top != null)
				top.Render(device, area);
		}

		/// <summary>
		/// Runs the specified input message.
		/// </summary>
		/// <param name="message">The input message to run.</param>
		public void SendInputMessage(InputMessage message)
		{
			// TODO: Use compile-time weaving instead of reflection.
			MethodInfo method =
				GetType().GetMethod("HandleInputMessage",
				                    BindingFlags.Instance |
				                    BindingFlags.Public, null,
				                    new[] {message.GetType()},
				                    new ParameterModifier[] {});
			Debug.Assert(method != null);
			method.Invoke(this, new object[] {message});
		}

		/// <summary>
		/// Updates the <see cref="GuiManager"/>.
		/// </summary>
		/// <param name="elapsed">The time since last update.</param>
		/// <exception cref="ObjectDisposedException">The object has
		/// already been disposed.</exception>
		public void Update(TimeSpan elapsed)
		{
			if (_disposed)
				throw new ObjectDisposedException("GuiManager");
		}
	}
}